
目前我们在渲染管线中都是使用的一些简单的材质来测试功能,下面我们支持一些复杂的材质,来创建一些更复杂多样的表面。本节将创建一种类似电路的材质。
14.1.1 基础纹理和自发光纹理
我们将使用下面两张纹理作为电路材质的基础纹理和自发光纹理。


创建一个使用Lit.shader的材质,然后使用这两张纹理,基础纹理Tiling的X设为2,自发光的Color设置为白色。

我们只想要电路的纹路是金属的,但现在的金属度和光滑度的属性是统一设置的,需要使用遮罩纹理(Mask Map)对球体表面进行修改。
14.2.1 MODS纹理
我们添加一张遮罩纹理来控制金属度和光滑度,一张遮罩纹理有RGBA 4个通道,其单个通道掩盖了不同的着色器属性。两者将占用遮罩纹理的两个通道,所以我们使用和Unity的HDRP相同格式的MODS纹理,它的RGBA通道分别存储了金属度(Metallic)、遮蔽(Occlusion)、细节(Detail)和光滑度(Smoothness)。我们使用该纹理的R和A通道来参与计算,由于此纹理存储的是遮罩数据而不是颜色,因此导入下面的纹理后确保sRGB (Color Texture)属性是禁用的,不然会导致GPU在采样纹理时错误应用伽马到线性空间的转换。

在Lit.shader的属性栏中添加遮罩纹理,默认为白色。
//遮罩纹理
[NoScaleOffset] _MaskMap("Mask (MODS)", 2D) = "white" {}

14.2.2 应用金属度和光滑度
1. 在LitInput.hlsl中定义一个GetMask方法获取采样后的数据。
TEXTURE2D(_MaskMap);
float4 GetMask (float2 baseUV)
{
return SAMPLE_TEXTURE2D(_MaskMap, sampler_BaseMap, baseUV);
}
2. 在继续之前,我们整理一下LitInput和UnlitInput中的代码,我们使用INPUT_PROP宏来代替UNITY_ACCESS_INSTANCED_PROP宏,这样看起来简短易读,然后检索所有使用UNITY_ACCESS_INSTANCED_PROP宏的代码进行替换。
#define INPUT_PROP(name) UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, name)
3. 在GetMetallic和GetSmoothness方法中使用金属度和光滑度乘以遮罩纹理的R和A通道值计算最终结果。
float GetMetallic(float2 baseUV)
{
float metallic = INPUT_PROP(_Metallic);
metallic *= GetMask(baseUV).r;
return metallic;
}
float GetSmoothness(float2 baseUV)
{
float smoothness = INPUT_PROP(_Smoothness);
smoothness *= GetMask(baseUV).a;
return smoothness;
}

14.2.3 应用遮挡
1. 我们的遮罩纹理G通道包含的是遮挡(Occlusion)数据,其理念是:像缝隙和孔这样的小凹陷区域大多被物体的其余部分遮盖,但如果这些特征仅以纹理表示,照明就会忽略这些特征。所以我们丢失的遮挡数据由遮罩纹理来提供,我们添加一个新的GetOcclusion方法来获取遮挡数据。
float GetOcclusion (float2 baseUV)
{
return GetMask(baseUV).g;
}
2. 将遮挡数据添加到Surface结构体中。
struct Surface
{
...
//遮挡数据
float occlusion;
};
3. 在LitPass文件的LitPassFragment方法中获取遮挡数据。
surface.occlusion = GetOcclusion(input.baseUV);
4. 遮挡只适用于间接的环境照明,直接光照则不受影响,因此当光照直接指向它们时,缝隙不会保持黑暗。所以我们仅使用遮挡数据调整IndirectBRDF返回的最终结果。
float3 IndirectBRDF (Surface surface, BRDF brdf, float3 diffuse, float3 specular)
{
...
return (diffuse * brdf.diffuse + reflection) * surface.occlusion;
}

5. 我们在着色器的属性栏中添加一个遮挡强度滑块来更精确地控制效果。
//遮挡强度
_Occlusion ("Occlusion", Range(0, 1)) = 1
6. 在LitInput的UnityPerMaterial缓冲区中声明它。
UNITY_DEFINE_INSTANCED_PROP(float, _Occlusion)
7. 最后在GetOcclusion方法中通过插值来调节遮挡数据。
float GetOcclusion (float2 baseUV)
{
float strength = INPUT_PROP(_Occlusion);
float occlusion = GetMask(baseUV).g;
occlusion = lerp(occlusion, 1.0, strength);
return occlusion;
}
我们再添加一个细节纹理丰富物体的表面,对其使用更高的Tiling进行采样,然后与基础纹理和遮罩纹理数据结合到一起,这使得表面信息更丰富,也提供更高的分辨率信息,当表面近距离观察时,基础纹理将像素化显示。
细节应该只稍微修改表面属性,因此我们再次将一些细节数据组合到单张非彩色纹理中,就像遮罩纹理一样。HDRP使用ANySNx格式,这意味着该细节纹理的R通道存储反照率(Albedo)调制,B通道存储光滑度(Smoothness)调制,AG通道存储一个细节法线向量的XY分量,我们目前还不支持法线向量,因此只使用RB通道。下面的贴图导入后记得禁用sRGB(Color Texture)属性。

14.3.1 细节UV坐标
1. 细节纹理应该比基础纹理使用更高的Tiling,所以我们会使用单独的UV,在着色器的属性栏中声明一个细节纹理属性,默认值为linearGrey,这样默认不会对材质有任何改变。
//细节纹理
_DetailMap("Details", 2D) = "linearGrey" {}

2. 在LitInput中声明细节纹理、采样器和缩放偏移属性,以及定义一个TransformDetailUV方法进行细节UV坐标的转换。
TEXTURE2D(_DetailMap);
SAMPLER(sampler_DetailMap);
UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
//提供纹理的缩放和偏移
UNITY_DEFINE_INSTANCED_PROP(float4, _BaseMap_ST)
UNITY_DEFINE_INSTANCED_PROP(float4, _DetailMap_ST)
...
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
//细节纹理UV变换
float2 TransformDetailUV (float2 detailUV)
{
float4 detailST = INPUT_PROP(_DetailMap_ST);
return detailUV * detailST.xy + detailST.zw;
}
3. 定义一个GetDetail方法获取细节纹理的采样数据。采样完后应该正确的解释它,0.5是中性值,较高的值应该使细节增加或变亮,较低的值应该使细节减少或变暗,因为我们将采样后的数据范围从[0,1]转换到[-1,1]。
float4 GetDetail (float2 detailUV)
{
float4 map = SAMPLE_TEXTURE2D(_DetailMap, sampler_DetailMap, detailUV);
return map * 2.0 - 1.0;
}
4. 在LitPass的LitPassVertex方法中转换UV坐标。
struct Varyings
{
...
float2 baseUV : VAR_BASE_UV;
float2 detailUV : VAR_DETAIL_UV;
...
};
Varyings LitPassVertex(Attributes input)
{
...
output.detailUV = TransformDetailUV(input.baseUV);
return output;
}
14.3.2 细节反照率
1. 要给反照率(Albedo)添加细节,我们需要给GetBase方法中添加一个细节UV参数,默认情况下为0,在采样完基础纹理后,紧接着对细节纹理进行采样,然后将两者的采样数据进行叠加后再乘以颜色值。
float4 GetBase(float2 baseUV, float2 detailUV = 0.0)
{
...
float4 detail = GetDetail(detailUV);
map += detail;
return map * color;
}
2. 在LitPassFragment方法中传递细节UV坐标。
float4 base = GetBase(input.baseUV, input.detailUV);
3. 然后再次调整GetBase方法,只有细节纹理的R通道数据会影响反照率,将其推向黑色或白色。可以通过将基础纹理颜色以及0或1的反照率信息根据反照率信息的绝对值进行插值来实现。这样只会影响反照率,而不影响基础纹理的Alpha通道。
float4 GetBase(float2 baseUV, float2 detailUV = 0.0)
{
...
float detail = GetDetail(detailUV).r;
//map += detail;
map.rgb = lerp(map.rgb, detail < 0.0 ? 0.0 : 1.0, abs(detail));
return map * color;
}

4. 现在增量效果要比变暗效果更强一些,因为我们是在线性空间中应用了修改,在伽马空间进行该操作视觉效果更好一些,可以通过对反照率的平方根进行插值而后再平方来近似处理。
float4 GetBase(float2 baseUV, float2 detailUV = 0.0)
{
...
map.rgb = lerp(sqrt(map.rgb), detail < 0.0 ? 0.0 : 1.0, abs(detail));
map.rgb *= map.rgb;
return map * color;
}
5. 现在细节已经应用到了整个表面,接下来我们获取遮罩细节数据,它存储在遮罩纹理的B通道,我们可以使用它来增强金色纹路的细节。
float mask = GetMask(baseUV).b;
map.rgb = lerp(sqrt(map.rgb), detail < 0.0 ? 0.0 : 1.0, abs(detail) * mask);

6. 我们的细节现在可能过于强烈,为了使细节反照率强度更加可控,在着色器的属性栏中添加一个滑块进行控制。
_DetailAlbedo("Detail Albedo", Range(0, 1)) = 1
7. 在UnityPerMaterial缓冲区中声明它,并在GetBase方法中和细节反照率相乘。
UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
...
UNITY_DEFINE_INSTANCED_PROP(float, _DetailAlbedo)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
float4 GetBase(float2 baseUV, float2 detailUV = 0.0)
{
...
float detail = GetDetail(detailUV).r * INPUT_PROP(_DetailAlbedo);
...
}
14.3.3 细节光滑度
1. 将细节光滑度以同样的方式应用进来,首先在着色器中添加调节细节光滑度强度的滑块。
_DetailSmoothness("Detail Smoothness", Range(0, 1)) = 1
2. 在UnityPerMaterial缓冲区中声明它,并在GetSmoothness方法中通过细节纹理的B通道得到细节光滑度数据并进行插值获取最终光滑度。
UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
...
UNITY_DEFINE_INSTANCED_PROP(float, _DetailSmoothness)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
float GetSmoothness(float2 baseUV, float2 detailUV = 0.0)
{
...
float detail = GetDetail(detailUV).b * INPUT_PROP(_DetailSmoothness);
float mask = GetMask(baseUV).b;
smoothness = lerp(smoothness, detail < 0.0 ? 0.0 : 1.0, abs(detail) * mask);
return smoothness;
}
3. 通过LitPassFragment方法传递细节纹理的UV坐标。
surface.smoothness = GetSmoothness(input.baseUV, input.detailUV);
14.3.4 淡化细节
细节只有在视觉上足够大时才比较重要,如果细节太小则不应该应用,因为这个会产生嘈杂的效果。Mip映射通常会模糊数据,但对于细节,我们希望更进一步,将其淡化,下图是满强度的细节效果:

细节纹理上有一个Fadeout Mip Maps属性,启用后Unity可以帮助我们淡化细节,然后通过一个范围滑块控制淡化开始和结束的Mip级别,Unity将Mip Map插值设为灰色,意味着贴图将变为中性,为此需要将纹理的滤波模式设置为三线性滤波模式。

下图是细节淡化后的效果:

法线纹理是通过修改模型表面的法线,为模型提供更多的细节,让模型看起来好像是凹凸不平的。我们这里使用切线空间的法线纹理,由于法线方向的分量范围在[-1,1],而像素分量范围为[0,1],因此需要做一个法线映射:pixel=(normal+1)/2。通常切线空间下的法线纹理看起来几乎是浅蓝色的,我们将下面的法线纹理导入后Texture Type设置为Normal map。

14.4.1 采样法线
1. 要采样法线纹理,首先在着色器的属性栏中声明法线纹理和一个控制法线强度的滑块。
//法线贴图
[NoScaleOffset] _NormalMap("Normals", 2D) = "bump" {}
_NormalScale("Normal Scale", Range(0, 1)) = 1

存储的法线的信息通常是RGB通道中的XYZ,但这不是最有效的方法,如果我们假设法线向量总是指向上方而从不指向下方,则我们可以忽略向上的分量。使用XY分量可以得出向上的Z分量,然后这些通道可以使用压缩纹理格式存储,来最大限度的减少精度损失。将XY分量存储在RG或AG中,这取决于纹理的格式,这将会改变纹理的外观,但Unity编辑器仅显示原纹理的预览和缩略图。
2. 法线纹理是否改变取决于目标平台,如果纹理未修改,则定义UNITY_NO_DXT5nm。我们可以使用UnpackNormalRGB方法转换采样的法线数据,通过法线反映射得到原先的[-1,1]范围的法线方向,否则使用UnpackNormalmapRGorAG方法进行转换,它针对DXT5nm和BC5格式将存于XY通道的法线信息转换到WY通道统一处理后放回XY通道,且得到原先的[-1,1]范围,然后基于XY计算Z的值。这两个方法都需要sample和scale参数,且方法在源码库文件Packing中定义。我们在Common.hlsl定义一个DecodeNormal方法来解码法线数据,得到原来的法线向量。
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Packing.hlsl"
//解码法线数据,得到原来的法线向量
float3 DecodeNormal (float4 sample, float scale)
{
#if defined(UNITY_NO_DXT5nm)
return UnpackNormalRGB(sample, scale);
#else
return UnpackNormalmapRGorAG(sample, scale);
#endif
}
DXT5nm的含义是?
DXT5(又叫BC3)是一种压缩格式,将纹理分割成4*4的像素块,每个方块有两种颜色,然后逐像素进行插值。用于颜色的位数每个颜色通道都不同。R和B各得到5位,G得到6位,A得到8位,这就是X坐标移动到A通道的原因之一。另一个原因是,RGB通道得到一个查找表,而A通道获得自己的查找表,这样可以隔离X和Y分量。
当DXT5用于存储法线向量时,它被称为DXT5nm,但是当使用高压缩质量时,Unity更喜欢BC7的压缩,此模式的工作原理相同,但每个通道位数可能有所不同。因此X通道不需要移动,最终的纹理会更大,因为两个通道都使用了更多的位,提高了纹理质量。UnpackNormalmapRGorAG方法可通过乘以R和A通道处理这两种方法,这要求未使用的通道设置为1,Unity会默认这样做。
3. 在LitInput文件中声明法线纹理和控制法线强度的属性,定义一个GetNormalTS方法采样纹理和解码法线得到原法线方向。
TEXTURE2D(_NormalMap);
UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
...
UNITY_DEFINE_INSTANCED_PROP(float, _NormalScale)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
//采样法线并解码法线向量
float3 GetNormalTS (float2 baseUV)
{
float4 map = SAMPLE_TEXTURE2D(_NormalMap, sampler_BaseMap, baseUV);
float scale = INPUT_PROP(_NormalScale);
float3 normal = DecodeNormal(map, scale);
return normal;
}
14.4.2 切线空间
因为纹理环绕几何形状,它们在对象空间和世界空间的方向并不统一,因此法线存储曲线的空间与几何表面相匹配,唯一不变的是空间与表面相切,这就是它被称为切线空间的原因,该空间Y轴正方向和表面法线匹配,除此之外它还有一个X正方向与表面相切,有了这两个分量我们就可以生成前向Z轴。
由于切线空间的X轴不是恒定的,因此需要将其定义为网格顶点数据的一部分。它有4个分量的切线向量,其中XYZ分量定义对象空间的坐标轴,W分量为-1或1,用来控制Z轴指向的方向。这用于翻转具有双边对称性网格的法线贴图,大多数动物都有这种对称性,因此相同的贴图可以用于网格两侧,从而将所需纹理大小减半。
1. 在Common.hlsl中创建NormalTangentToWorld方法,用于将法线从切线空间转换到世界空间。如果我们有世界空间的法线和切向量,则可以构建出从切线空间到世界空间的转换矩阵,我们可以调用CreateTangentToWorld方法,传递世界空间的法线、切线的XYZ分量和切线的W分量来做到这一点。然后调用TransformTangentToWorld方法,传递切线空间的法线和切线空间到世界空间的转换矩阵。
//将法线从切线空间转换到世界空间
float3 NormalTangentToWorld (float3 normalTS, float3 normalWS, float4 tangentWS)
{
//构建切线到世界空间的转换矩阵,需要世界空间的法线、世界空间的切线的XYZ和W分量
float3x3 tangentToWorld = CreateTangentToWorld(normalWS, tangentWS.xyz, tangentWS.w);
return TransformTangentToWorld(normalTS, tangentToWorld);
}
2. 在LitPass中的顶点输入结构体中声明对象空间的切线,片元输入结构体中声明世界空间的切线。
struct Attributes
{
...
//对象空间的切线
float4 tangentOS : TANGENT;
GI_ATTRIBUTE_DATA
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
...
//世界空间的切线
float4 tangentWS : VAR_TANGENT;
GI_VARYINGS_DATA
UNITY_VERTEX_INPUT_INSTANCE_ID
};
3. 在顶点函数中使用TransformObjectToWorldDir方法将对象空间切线的XYZ分量转换到世界空间,得到世界空间中的切线。
output.normalWS = TransformObjectToWorldNormal(input.normalOS);
//计算世界空间的切线
output.tangentWS = float4(TransformObjectToWorldDir(input.tangentOS.xyz), input.tangentOS.w);
4. 在片元函数中调用NormalTangentToWorld方法得到世界空间中的法线。
surface.normal = NormalTangentToWorld(GetNormalTS(input.baseUV), input.normalWS, input.tangentWS);

14.4.3 阴影偏差的插值法线
1. 扰动法线向量适合照亮表面,但我们也可以使用片元法线偏差对阴影采样,但应使原始的表面法线,我们在Surface结构体中声明它。
struct Surface
{
float3 position;
float3 normal;
float3 interpolatedNormal;
...
};
2. 在片元函数中得到法线向量,这种情况下我们无需进行法线向量的归一化。
surface.interpolatedNormal = input.normalWS;
surface.viewDirection = normalize(_WorldSpaceCameraPos - input.positionWS);
3. 然后在Shadows.hlsl的GetCascadedShadow方法中使用该插值法线。
float GetCascadedShadow(DirectionalShadowData directional, ShadowData global, Surface surfaceWS)
{
//计算法线偏移
float3 normalBias = surfaceWS.interpolatedNormal * (directional.normalBias * _CascadeData[global.cascadeIndex].y);
...
if (global.cascadeBlend < 1.0)
{
normalBias = surfaceWS.interpolatedNormal *(directional.normalBias * _CascadeData[global.cascadeIndex + 1].y);
...
}
return shadow;
}
14.4.4 细节法线
我们还可以包含一个细节法线纹理,虽然HDRP在单张纹理中将法线细节和反照率与光滑度结合在一块,但我们仍可以使用单独的纹理。将下面的贴图导入并设置Texture Type为Normal map,然后启用Fadeout Mip Maps属性。

为什么两张纹理不合并?
虽然这样效率会高一些,但是生成这样的贴图比较困难,生成Mip Map时应将法线向量和其它通道数据区别对待,而Unity的纹理导入器无法做到这一点,而且Unity在渐变Mip Map时会忽略Alpha通道,因此该通道中的数据不会正确过渡,这就需要在Unity外部或使用脚本生成Mip Map,即便如此我们仍需要手动解码法线数据,而不是依赖UnpackNormalmapRGorAG方法。
1. 在着色器的属性栏中添加细节法线纹理和细节法线强度调节滑块。
Properties
{
...
[NoScaleOffset] _DetailNormalMap("Detail Normals", 2D) = "bump" {}
_DetailNormalScale("Detail Normal Scale", Range(0, 1)) = 1
...
}

2. 声明细节法线纹理和强度属性,在LitInput的GetNormalTS方法中添加细节UV并采样细节法线纹理,我们可以获取遮罩纹理的B通道数据来当做细节法线强度应用,最后调用BlendNormalRNM方法,用于在基础法线周围旋转细节法线。
TEXTURE2D(_DetailNormalMap);
UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
...
UNITY_DEFINE_INSTANCED_PROP(float, _DetailNormalScale)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
float3 GetNormalTS (float2 baseUV, float2 detailUV = 0.0)
{
...
map = SAMPLE_TEXTURE2D(_DetailNormalMap, sampler_DetailMap, detailUV);
scale = INPUT_PROP(_DetailNormalScale) * GetMask(baseUV).b;
float3 detail = DecodeNormal(map, scale);
normal = BlendNormalRNM(normal, detail);
return normal;
}
3. 最后在片元函数将细节UV传递过去。
surface.normal = NormalTangentToWorld(GetNormalTS(input.baseUV, input.detailUV), input.normalWS, input.tangentWS);

重要提示:
现在我发现了一个Bug,当我的Shader支持法线纹理时,且我的渲染管线启用了SRP Batcher。物体材质在视图中移动观察时会出现如下图的问题:

这时SRP Batcher在Frame Debugger中有这个提示:

我不确定这个问题是我的引擎的问题还是SRP Batcher的bug还是自己某块代码导致的问题,目前解决方案是禁用我的渲染管线的SRP Batcher。这里Mark一下,以后找到原因了我再修复。
并不是每个材质都会需要使用我们当前所支持的所有纹理,如果材质未指定纹理,虽然对结果无影响,但着色器仍会使用默认纹理来进行所有相关逻辑的计算,我们可以添加一些切换开关等显示控制选项来完成这个工作。
14.5.1 输入配置
1. 我们先对LitInput.hlsl的代码做一些优化。日后可能会调用这个文件里面的很多方法获取某些数据,但它们的传参数也可能是不固定的,虽然目前大多数只需要一个UV坐标的传参。我们把这些输入数据整合起来,定义一个InputConfig结构体,里面目前先声明一个基础纹理的UV和细节纹理的UV属性,然后再定义一个GetInputConfig方法用于获取输入配置结构体的数据。
struct InputConfig
{
float2 baseUV;
float2 detailUV;
};
InputConfig GetInputConfig(float2 baseUV, float2 detailUV = 0.0)
{
InputConfig c;
c.baseUV = baseUV;
c.detailUV = detailUV;
return c;
}
2. 下面我们对除了TransformBaseUV和TransformDetailUV方法之外的所有方法的传参统一使用InputConfig参数,然后修改各方法内部的调用,下面以GetBase方法为例:
float4 GetBase(InputConfig c)
{
float4 map = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, c.baseUV);
float4 color = INPUT_PROP( _BaseColor);
float detail = GetDetail(c).r * INPUT_PROP(_DetailAlbedo);
float mask = GetMask(c).b;
...
}
3. 然后在片元函数调用GetInputConfig获取结构体,并调整各Get方法的相关调用传参。
float4 LitPassFragment(Varyings input) : SV_TARGET
{
...
InputConfig config = GetInputConfig(input.baseUV, input.detailUV);
float4 base = GetBase(config);
#if defined(_CLIPPING)
//透明度低于阈值的片元进行舍弃
clip(base.a - GetCutoff(config));
#endif
...
surface.metallic = GetMetallic(config);
surface.occlusion = GetOcclusion(config);
surface.smoothness = GetSmoothness(config);
surface.fresnelStrength = GetFresnel(config);
...
color += GetEmission(config);
return float4(color, GetFinalAlpha(surface.alpha));
}
最后我们还需要调整MetaPass,ShadowCasterPass和UnlitPass也使用新的调用方法,并且UnlitInput.hlsl也进行相关的定义和调用,代码不再贴出。
14.5.2 可选的法线纹理
1. 首先在着色器的属性栏中定义一个控制法线开关的切换选项。
[Toggle(_NORMAL_MAP)] _NormalMapToggle("Normal Map", Float) = 0

2. 然后在CustomLit Pass中声明一个启用法线功能的关键字。
#pragma shader_feature _NORMAL_MAP
3. 在片元函数中进行判断,若启用了法线则将切线空间的法线转换到世界空间。如果未启用,则仅对法线向量进行归一化。
struct Varyings
{
...
#if defined(_NORMAL_MAP)
//世界空间的切线
float4 tangentWS : VAR_TANGENT;
#endif
...
};
Varyings LitPassVertex(Attributes input)
{
...
#if defined(_NORMAL_MAP)
//计算世界空间的切线
output.tangentWS = float4(TransformObjectToWorldDir(input.tangentOS.xyz), input.tangentOS.w);
#endif
...
}
float4 LitPassFragment(Varyings input) : SV_TARGET
{
...
#if defined(_NORMAL_MAP)
surface.normal = NormalTangentToWorld(GetNormalTS(config), input.normalWS, input.tangentWS);
surface.interpolatedNormal = input.normalWS;
#else
surface.normal = normalize(input.normalWS);
surface.interpolatedNormal = surface.normal;
#endif
...
}
14.5.3 可选的遮罩纹理
1. 在LitInput的InputConfig结构体声明一个bool值用于判断是否使用了遮罩纹理,默认设置为false。
struct InputConfig
{
...
bool useMask;
};
InputConfig GetInputConfig(float2 baseUV, float2 detailUV = 0.0)
{
...
c.useMask = false;
return c;
}
2. 调整GetMask方法,若没有使用则不进行遮罩纹理的采样。
float4 GetMask (InputConfig c)
{
if (c.useMask)
{
return SAMPLE_TEXTURE2D(_MaskMap, sampler_BaseMap, c.baseUV);
}
return 1.0;
}
3. 在Shader的属性栏中同样添加一个使用遮罩纹理的切换开关,并声明一个相关联的关键字。
[Toggle(_MASK_MAP)] _MaskMapToggle("Mask Map", Float) = 0
#pragma shader_feature _MASK_MAP
4. 最后在片元函数根据关键字的启用来设置bool值。
float4 LitPassFragment(Varyings input) : SV_TARGET
{
...
InputConfig config = GetInputConfig(input.baseUV, input.detailUV);
#if defined(_MASK_MAP)
config.useMask = true;
#endif
...
}
14.5.4 可选的细节纹理
1. 跟遮罩纹理一样,也配置一个bool值。
struct InputConfig
{
...
bool useDetail;
};
InputConfig GetInputConfig(float2 baseUV, float2 detailUV = 0.0)
{
...
c.useDetail = false;
return c;
}
2. 当bool值为true时才进行采样。
float4 GetDetail (InputConfig c)
{
if (c.useDetail)
{
float4 map = SAMPLE_TEXTURE2D(_DetailMap, sampler_DetailMap, c.detailUV);
return map * 2.0 - 1.0;
}
return 0.0;
}
3. 然后调整GetBase、GetSmoothness和GetNormalTS方法,虽然可以跳过对细节纹理采样,但仍可以包含细节。
float4 GetBase(InputConfig c)
{
...
if (c.useDetail)
{
float detail = GetDetail(c).r * INPUT_PROP(_DetailAlbedo);
float mask = GetMask(c).b;
map.rgb = lerp(sqrt(map.rgb), detail < 0.0 ? 0.0 : 1.0, abs(detail) * mask);
map.rgb *= map.rgb;
}
return map * color;
}
float GetSmoothness(InputConfig c)
{
...
if (c.useDetail)
{
float detail = GetDetail(c).b * INPUT_PROP(_DetailSmoothness);
float mask = GetMask(c).b;
smoothness = lerp(smoothness, detail < 0.0 ? 0.0 : 1.0, abs(detail) * mask);
}
return smoothness;
}
float3 GetNormalTS (InputConfig c)
{
...
if (c.useDetail)
{
map = SAMPLE_TEXTURE2D(_DetailNormalMap, sampler_DetailMap, c.detailUV);
scale = INPUT_PROP(_DetailNormalScale) * GetMask(c).b;
float3 detail = DecodeNormal(map, scale);
normal = BlendNormalRNM(normal, detail);
}
return normal;
}
4. 然后在属性栏中添加使用细节纹理的切换开关和声明其相关的关键字。
[Toggle(_DETAIL_MAP)] _DetailMapToggle("Detail Maps", Float) = 0
#pragma shader_feature _DETAIL_MAP
5. 当使用了细节纹理时,我们才声明和计算细节纹理UV。
struct Varyings
{
...
#if defined(_DETAIL_MAP)
float2 detailUV : VAR_DETAIL_UV;
#endif
...
};
//顶点函数
Varyings LitPassVertex(Attributes input)
{
...
#if defined(_DETAIL_MAP)
output.detailUV = TransformDetailUV(input.baseUV);
#endif
return output;
}
6. 最后在片元函数中根据关键字的启用状态来设置细节信息。
InputConfig config = GetInputConfig(input.baseUV);
#if defined(_DETAIL_MAP)
config.detailUV = input.detailUV;
config.useDetail = true;
#endif
14|复杂纹理
提交
暂无评论